import MIDI from '../../lib/midi'
import { sum } from '../utils/helpers'

// 当DOM加载完成后执行的函数，用于加载MIDI插件
window.onload = () => {
  // 加载MIDI插件，参数包括音色字体的URL和使用的乐器类型
  MIDI.loadPlugin({
    soundfontUrl: './soundfont/', // 音色字体的URL路径
    instrument: 'acoustic_grand_piano', // 选择的乐器，这里选择的是“ acoustic_grand_piano ”（ acoustic_grand_piano ）

    // 加载过程中的回调函数，用于显示加载进度
    onprogress(state, progress) {
      console.log(state, progress) // 输出当前的加载状态和进度
    },
    // 插件加载成功后的回调函数
    onsuccess() {
      // 在这里可以编写插件加载成功后需要执行的代码
      // 目前，具体的音符播放逻辑被注释掉了
      // 以下是一段示例代码，展示如何播放一个音符
      // var delay = 0  // 每个音符之间有四分之一秒的延迟
      // var note = 50  // MIDI音符编号
      // var velocity = 127  // 音符的强度
      // // 播放音符
      // MIDI.setVolume(0, 127) // 设置音量
      // MIDI.noteOn(0, note, velocity, delay) // 开始播放音符
      // MIDI.noteOff(0, note, delay + 0.75) // 在指定延迟后停止播放音符
    }
  })
}


// 用于存储定时器标识的数组
const tos = []


/**
 * 播放音乐元素的函数
 * 该函数会根据传入的音乐元素类型，调用相应的播放方法
 */
export function play() {
  // 清除所有的定时器，以确保上一个播放动作能够停止
  tos.forEach(clearTimeout);
  // 重置定时器数组，避免残留已清除的定时器
  tos.length = 0;

  // 获取当前对象，即需要播放的音乐元素
  const obj = this.obj;
  // 记录播放动作的日志信息
  console.log("huoq ----play---obj>", obj);

  // 延迟200毫秒后开始播放，以给予一些准备时间
  setTimeout(() => {
    // 根据不同的音乐元素类型，调用相应的播放函数
    switch (obj.name) {
      case 'score': playScore(obj); break; // 播放整个乐谱
      case 'part': playPart(obj); break; // 播放乐谱中的一个部分
      case 'staff': playStaff(obj); break; // 播放一个简谱行
      case 'cell': playCell(obj); break; // 播放一个节拍单位内的音符
      case 'note': playNote(obj); break; // 播放单个音符
      case 'chord': playChord(obj); break; // 播放和弦
      case 'multipart': playMultipart(obj); break; // 播放包含多个部分的音乐片段
    }
  }, 200);
}


// 暂停播放的函数，目前未实现具体逻辑
export function pause() {

}


/**
 * 停止播放的函数，清除所有定时器
 *
 * 此函数的目的是遍历存储计时器的数组(tos),
 * 并逐个清除这些计时器最后，将数组清空，以便下一次使用
 *
 * @export
 * @function stop
 */
export function stop() {
  // 打印当前tos数组，以便调试或日志记录
  console.log("获取-stop-->", tos)
  // 遍历tos数组，对每个元素调用clearTimeout函数以清除计时器
  tos.forEach(clearTimeout)
  // 清空tos数组，为下一次使用做准备
  tos.length = 0
}


/**
 * 播放MIDI音符，设置音量并控制音符的起始与结束时间
 *
 * 该函数用于播放一个MIDI音符，并在音符播放结束后执行一个回调函数（如果有提供的话）
 *
 * @param {number} t - 音符开始播放的时间（以拍为单位）
 * @param {number} midiNumber - MIDI音符编号，表示要播放的具体音符
 * @param {number} dur - 音符的持续时间（以拍为单位）
 * @param {function} onended - 音符播放结束后的回调函数，如果是同步函数，则在音符结束后立即执行
 */
const midiPlay = (t, midiNumber, dur, onended) => {
  // 设置音符的强度，值范围为0-127，表示音符的响度
  const velocity = 100
  // 设置默认音轨（0号音轨）的音量为100
  MIDI.setVolume(0, 100)
  // 在指定时间t，播放指定的MIDI音符，设置其强度（velocity）
  MIDI.noteOn(0, midiNumber, velocity, t)
  // 在指定时间t加上持续时间dur后，停止播放该MIDI音符
  MIDI.noteOff(0, midiNumber, t + dur)
  // 如果提供了结束回调函数，则在音符结束后设置一个定时器来执行该回调
  if (onended) {
    setTimeout(onended, (t + dur) * 1000)
  }
}


/**
 * 根据给定的音符，获取连音符组成的数组
 * 这个函数主要用于处理带有连音线的音符，通过遍历找到所有相连的音符，并返回一个音符数组
 * @param {Object} note - 一个音符对象，该音符可能带有连音信息
 * @returns {Array} - 包含所有与给定音符相连的音符的数组
 */
const getTiedNotes = note => {
  // 初始化一个包含当前音符的数组
  const notes = [note]
  // 从当前音符的下一个连音符开始遍历
  let nnote = note.tie.nextNote
  while (nnote) {
    // 将遍历到的音符添加到数组中
    notes.push(nnote)
    // 如果当前音符还有连音信息，则更新到下一个连音符
    if (nnote.tie) nnote = nnote.tie.nextNote
  }
  // 返回包含所有相连音符的数组
  return notes
}


/**
 * 获取连音符对应的时值和音高对数组
 * 此函数旨在处理绑定点和音高的配对，通过遍历给定的绑定点和音高序列，生成一个包含所有配对的数组
 * @param {Object} notation - 一个包含绑定点(dt)和音高(pitch)的对象
 * @returns {Array} - 返回一个对象数组，每个对象包含一个绑定点(dt)和音高(pitch)的配对
 */
const getTiedDtPitchPairs = ({ dt, pitch }) => {
  console.log("获取--getTiedDtPitchPairs->", dt, pitch)
  // 初始化配对数组，首先将当前的绑定点和音高加入数组
  const pairs = [{ dt, pitch }]

  /**
   * 移动到下一个配对
   * 此函数通过检查当前音高或绑定点的绑定点信息来更新dt和pitch，以指向下一个配对
   */
  const nextPair = () => {
    const { tie } = pitch || dt // 如果pitch存在，则使用pitch的tie属性，否则使用dt的tie属性
    dt = tie.nextNote // 更新dt为下一个绑定点
    pitch = tie.nextPitch // 更新pitch为下一个音高
  }

  // 首先调用nextPair以准备第一个实际的下一个配对
  nextPair()
  // 循环直到没有更多的绑定点
  while (dt) {
    // 将当前的绑定点和音高配对添加到数组中
    pairs.push({ dt, pitch })
    // 移动到下一个配对
    nextPair()
  }
  // 返回绑定点和音高的配对数组
  return pairs
}


/**
 * 播放连音符对，并在结束后停止
 *
 * 该函数用于播放一组连音对，根据给定的音高和音对信息，在指定的上下文中进行播放
 * 它首先遍历每个音对并播放它们，然后计算所有音对的总时长
 * 最后，设置一个停止播放的处理程序，在播放结束时调用
 *
 * @param {Object} pitch - 包含midiNumber属性的对象，用于指定要播放的音高
 * @param {Array} pairs - 一个包含音对信息的数组，每个音对是一个对象，可能包含dt和pitch属性
 * @param {Object} context - 用于指定播放上下文的对象
 */
const playTiedPairs = (pitch, pairs, context) => {
  // 遍历音对数组，播放每个音对
  pairs.forEach(({ dt, pitch }) => {
    if (pitch) pitch.onplay(); else dt.onplay()
  })

  // 计算所有音对的总时长
  const dur = sum(pairs.map(({ dt }) => getDur(dt)))

  // 定义停止播放的处理程序
  const stopHandler = () => pairs.forEach(({ dt, pitch }) => {
    if (pitch) pitch.onstop(); else dt.onstop()
  })

  // 在指定上下文中，从MIDI音高0开始播放指定音高的音符，持续dur时间，并设置停止处理程序
  midiPlay(0, pitch.midiNumber, dur, stopHandler, context)
}


/**
 * 处理和弦播放逻辑，包括播放和停止和弦
 * @param {Array} pairs - 包含和弦对象的数组
 */
const chordOnplay = pairs => {
  // 遍历数组，找到名称为'chord'的对象，并调用其onplay方法
  pairs.forEach(({ dt }) => { if (dt.name === 'chord') dt.onplay() })

  // 计算所有和弦的持续时间总和
  const dur = sum(pairs.map(({ dt }) => getDur(dt)))

  // 设置定时器，等待和弦播放完毕后，停止所有播放的和弦
  setTimeout(() => {
    // 遍历数组，找到名称为'chord'的对象，并调用其onstop方法
    pairs.forEach(({ dt }) => { if (dt.name === 'chord') dt.onstop() })
  }, dur * 1000)
}


/**
 * 获取音符时间对象的秒数 音符的时值
 *
 * 该函数接受一个音符时间对象（dt）作为参数，并返回该对象中 duration 属性的 seconds 值
 * 主要用于从一个复杂的数据结构中提取出音符时间的秒数部分
 *
 * @param {Object} dt - 包含音符时间信息的对象，其中 duration 属性包含音符时间的秒数
 * @returns {number} - 返回提取出来的音符时间的秒数
 */
const getDur = dt => dt.duration.seconds


// 播放整份乐谱
// @param {Object} score - 乐谱对象，包含乐谱的所有部分
// @param {Object} context - 上下文对象，包含播放所需的环境信息
// 无返回值
const playScore = (score, context) => {
  // 遍历乐谱的每一个部分，并依次播放
  score.body.parts.forEach(part => playPart(part, context))
}


/**
 * 播放乐谱中的一个部分
 *
 * 该函数通过遍历部分中的每一个音符，并使用给定的上下文来播放它们，
 * 实现了播放乐谱的一个子任务。这对于分解复杂任务为简单子任务的处理方式非常有用，
 * 因为它允许每个音符的播放逻辑独立实现。
 *
 * @param {Object} part - 一个包含乐谱部分信息的对象，通常这会是一个结构化的对象，
 *                        其中包含有音符(staves)等元素的信息。
 * @param {Object} context - 执行播放的上下文环境，这个上下文包含了播放音符所需的所有信息和方法。
 *                           它允许函数在不知道具体实现细节的情况下，调用正确的方法来播放音符。
 */
const playPart = (part, context) => {
  part.staves.forEach(staff => playStaff(staff, context))
}


// 播放简谱的函数
// 参数:
// - staff: 一个包含简谱信息的对象，其中cells属性是一个包含每个音符信息的数组
// - context: 音频上下文，用于播放音符
// 该函数通过遍历简谱中的每个音符，并调用playCell函数来播放每个音符，从而实现整个简谱的播放
const playStaff = (staff, context) => {
  // 遍历简谱中的每个音符
  staff.cells.forEach(cell => {
    // 播放当前音符
    playCell(cell, context)
  })
}


// 播放小节的函数
const playCell = (cell, context) => {
  console.log("huoq =----playCell--->",cell, context)
  // 遍历单元格中的数据
  cell.data.forEach(dt => {
    // 根据不同的音乐元素执行相应的操作
    // console.log("--playCell-----dt->", dt)
    switch (dt.name) {
      case 'note':
        // 播放单个音符
        playNote(dt, context)
        break
      case 'rest':
        // 遇到休止符时不进行任何操作 0
        break
      case 'chord':
        // 播奏和弦
        playChord(dt, context)
        break
      case 'multipart':
        // 处理多声部音乐
        playMultipart(dt, context)
    }
  })
  console.log("获取===>", cell.rightBar.value)
  // 当前小节的右边界
  // if(cell.rightBar.value == ':|') {
  //
  // }

}

// 处理重复播放的音乐片段
const playRepeatNote = (multipart, context) => {
  // 获取重复次数和重复位置
  const { times, pos } = multipart

  // 遍历重复位置数组，依次播放音乐片段
  pos.forEach(p => {
    // 根据重复位置的类型，执行不同的播放逻辑
    switch (p.type) {
      case 'begin':
        // 开始位置，播放音乐片段
        playCell(p, context)
        break
      case 'end':
        // 结束位置，停止播放音乐片段
    }
  })
}

// 播放单个音符，处理连音
const playNote = (note, context) => {
  // 解构赋值，从note对象中提取pitch和duration属性
  const { pitch, duration } = note

  console.log("huqo --playNote---->", note)

  // 将来执行的函数加入到setTimeout队列中，以达到延迟执行的效果
  tos.push(setTimeout(() => {
    // 执行音符的播放操作，并输出日志信息
    console.log(`play: ${note}`, pitch.frequency)

    // 从note对象中解构赋值获取tie属性
    const { tie } = note
    console.log("hoq ---playNote-==tie=>", tie)

    // 判断音符是否使用了连音（tie）技巧
    if (tie) {
      // 检查连音类型，如果不是开始连音，则不执行下面的逻辑
      if (tie.type !== 'begin') return

      // 获取连音音符对
      const pairs = getTiedDtPitchPairs({ dt: note })

      // 播放连音音符对
      playTiedPairs(pitch, pairs, context)

      // 触发连音音符的onplay事件
      chordOnplay(pairs)
    } else {
      // 如果没有连音，触发音符的onplay事件
      note.onplay()

      // 获取音符的持续时间
      const dur = getDur(note)

      // 使用MIDI播放音符
      midiPlay(0, pitch.midiNumber, dur, () => note.onstop(), context)
    }
  }, note.t * 1000))

  console.log("获取-playNote-->", tos, note.t)

}

// 播放和弦的函数
const playChord = (chord, context) => {
  // 获取和弦的持续时间
  const dur = getDur(chord)
  // 使用setTimeout来延迟播放和弦
  tos.push(setTimeout(() => {
    // console.log(`play: ${chord}`)

    // 对于有连音线的和弦，调用chordOnplay()
    if (chord.tie && chord.tie.type === 'begin') {
      const pairs = getTiedDtPitchPairs({ dt: chord })
      chordOnplay(pairs)
    } else {
      // 没有连音线，则直接调用onplay()和onstop()
      chord.onplay()
      setTimeout(() => chord.onstop(), getDur(chord) * 1000)
    }

    // 播放和弦中的每个音符
    chord.pitches.forEach(pitch => {
      // 如果音符有连音线
      if (pitch.tie) {
        // 只处理连音线开始的音符
        if (pitch.tie.type !== 'begin') return
        const pairs = getTiedDtPitchPairs({ dt: chord, pitch })
        playTiedPairs(pitch, pairs, context)
      } else {
        // 没有连音线的音符，直接播放
        pitch.onplay()
        midiPlay(0, pitch.midiNumber, dur, () => pitch.onstop(), context)
      }
    })
  }, chord.t * 1000))
}


// 播放多重音轨的函数
// @param multipart 包含多层音轨的对象，每一层音轨将被独立播放
// @param context 音频处理的上下文环境，用于访问音频功能
const playMultipart = (multipart, context) => {
  // 遍历多重音轨的每一层并播放
  multipart.layers.forEach(layer => {
    playCell(layer, context)  // layer是cell的一个子集，这里调用playCell函数来播放当前层
  })
}

